// Copyright (c) 2009, Philip Tucker
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the <organization> nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY Philip Tucker 'AS IS' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL Philip Tucker BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package com.honkentuber.android;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.location.Address;
import android.location.Location;
import android.net.Uri;
import android.net.Uri.Builder;
import android.util.Log;


/**
 * This class wraps the web-based RESTful API for Google search.
 * code.google.com/apis/ajaxsearch/documentation/reference.html
 * TODO(ptucker) convenience methods for other types.
 */
public class GoogleSearcher {
  private static final String LOG_ID = GoogleSearcher.class.getSimpleName();
  
  private final String referer;

  private final Uri baseUri;
  
  private final Locale locale;
  
  /**
   * code.google.com/apis/ajaxsearch/documentation/reference.html#_intro_fonje
   */
  public enum Type {
    WEB("/ajax/services/search/web"),
    LOCAL("/ajax/services/search/local"),
    VIDEO("/ajax/services/search/video"),
    BLOGS("/ajax/services/search/blogs"),
    NEWS("/ajax/services/search/news"),
    BOOKS("/ajax/services/search/books"),
    IMAGES("/ajax/services/search/images"),
    PATENT("/ajax/services/search/patent");

    private final String path;

    private Type(String path) {
      this.path = path;
    }

    public String getPath() {
      return path;
    }
  }
  
  public enum SafeSearch {
    ACTIVE("active"),
    MODERATE("moderate"),
    OFF("off");

    private final String value;

    private SafeSearch(String value) {
      this.value = value;
    }

    public String getValue() {
      return value;
    }
  }

  /**
   * @see GoogleSearcher#GoogleSearcher(String, String, Locale)
   */
  public GoogleSearcher(String apiKey, String referer) {
    this(apiKey, referer, null);
  }
  
  /**
   * @param apiKey Search API key.
   * @param referer Referer URL.
   * @param locale Locale.
   */
  public GoogleSearcher(String apiKey, String referer, Locale locale) {
    this.referer = referer;
    this.locale = locale == null ? Locale.ENGLISH : locale;
    
    Builder builder = new Uri.Builder();
    builder.scheme("http").authority("ajax.googleapis.com");
    builder.appendQueryParameter("v", "1.0");
    builder.appendQueryParameter("key", apiKey);
    builder.appendQueryParameter("rsz", "small"); // 4
    builder.appendQueryParameter("hl", locale.toString());
    this.baseUri = builder.build();
  }

  /**
   * Unbounded search.
   * 
   * @see GoogleSearcher#local(String, Location, Location, Location)
   */
  public List<Address> local(String query, Location center) 
      throws IOException, JSONException {
    return local(query, center, null, null);
  }
  
  /**
   * code.google.com/apis/ajaxsearch/documentation/reference.html#_fonje_local
   * 
   * @param query Serach terms.
   * @param center Center of search.
   * @param lowerLeft Lower left of bound, or null if search not bounded.
   * @param upperRight Upper right of bound, or null if search not bounded.
   * @return Results.
   */
  public List<Address> local(String query, Location center,
      Location lowerLeft, Location upperRight) throws IOException,
      JSONException {
    Map<String, String> params = new HashMap<String, String>();
    params.put("mrt", "localonly");
    params.put("q", query);
    if (center != null) {
      params.put("sll", sllValue(center));
    }
    if (lowerLeft != null && upperRight != null) {
      params.put("sspn", sspnValue(lowerLeft, upperRight));
    }
    return toAddresses(search(Type.LOCAL, params));
  }

  /**
   * Convert JSON results to Address objects.
   */
  private List<Address> toAddresses(List<JSONObject> jsonResults) {
    List<Address> results = new ArrayList<Address>();
    for (JSONObject jsonResult : jsonResults) {
      try {
        Address result = new Address(locale);
        result.setLatitude(jsonResult.getDouble("lat"));
        result.setLongitude(jsonResult.getDouble("lng"));

        JSONArray addressLines = jsonResult.getJSONArray("addressLines");
        for (int j = 0; j < addressLines.length(); ++j) {
          result.setAddressLine(j, addressLines.getString(j));
        }
        result.setLocality(jsonResult.getString("city"));
        result.setAdminArea(jsonResult.getString("region"));
        result.setCountryName(jsonResult.getString("country"));
        result.setFeatureName(jsonResult.getString("titleNoFormatting"));
        results.add(result);
      } catch (JSONException ex) {
        Log.e(LOG_ID, "failure reading JSON", ex);
      }
    }
    return results;
  }

  /**
   * @see GoogleSearcher#web(String, SafeSearch, boolean)
   */
  public List<JSONObject> web(String query) throws IOException, JSONException {
    return web(query, null, true);
  }

  /**
   * code.google.com/apis/ajaxsearch/documentation/reference.html#_fonje_web
   * 
   * @param query Serach terms.
   * @param safeSearch Safety level.
   * @param filter Duplicate content filter
   * @return Results.
   */
  public List<JSONObject> web(String query, SafeSearch safeSearch, 
      boolean filter) throws IOException, JSONException {
    Map<String, String> params = new HashMap<String, String>();
    params.put("q", query);
    if (safeSearch != null) {
      params.put("safe", safeSearch.getValue());
    }
    if (!filter) {
      params.put("filter", "0");
    }
    return search(Type.WEB, params);
  }

  /**
   * @see GoogleSearcher#video(String, boolean)
   */
  public List<JSONObject> video(String query) 
      throws IOException, JSONException {
    return video(query, false);
  }
  
  /**
   * code.google.com/apis/ajaxsearch/documentation/reference.html#_fonje_video
   * 
   * @param query Search terms.
   * @param sortByDate If true sort by date. Otherwise sort by relevance.
   * @return Results.
   */
  public List<JSONObject> video(String query, boolean sortByDate) 
      throws IOException, JSONException {
    Map<String, String> params = new HashMap<String, String>();
    params.put("q", query);
    if (sortByDate) {
      params.put("scoring", "d");
    }
    return search(Type.VIDEO, params);
  }

  /**
   * @see GoogleSearcher#blog(String, boolean)
   */
  public List<JSONObject> blog(String query) 
      throws IOException, JSONException {
    return blog(query, false);
  }
  
  /**
   * code.google.com/apis/ajaxsearch/documentation/reference.html#_fonje_blog
   * 
   * @param query Search terms.
   * @param sortByDate If true sort by date. Otherwise sort by relevance.
   * @return Results.
   */
  public List<JSONObject> blog(String query, boolean sortByDate) 
      throws IOException, JSONException {
    Map<String, String> params = new HashMap<String, String>();
    params.put("q", query);
    if (sortByDate) {
      params.put("scoring", "d");
    }
    return search(Type.BLOGS, params);
  }

  /**
   * @return Value for sll value, or search center point in lat,lon.
   */
  private static String sllValue(Location center) {
    StringBuilder sllValue = new StringBuilder();
    sllValue.append(Geo.toLatLonString(center));
    return sllValue.toString();
  }

  /**
   * @return Value for sll value, or search span in lat,lon delta.
   */
  private static String sspnValue(Location lowerLeft, Location upperRight) {
    double latSpan = upperRight.getLatitude() - lowerLeft.getLatitude();
    double lonSpan = upperRight.getLongitude() - lowerLeft.getLongitude();
    StringBuilder sspnValue = new StringBuilder();
    sspnValue.append(latSpan).append(",").append(lonSpan);
    return sspnValue.toString();
  }

  /**
   * code.google.com/apis/ajaxsearch/documentation/reference.html#_intro_fonje
   */
  public List<JSONObject> search(Type type, Map<String, String> params)
      throws IOException, JSONException {
    // build request
    Uri.Builder builder = baseUri.buildUpon();
    builder.path(type.getPath());
    if (params != null) {
      for (Map.Entry<String, String> p : params.entrySet()) {
        builder = builder.appendQueryParameter(p.getKey(), p.getValue());
      }
    }
    Uri uri = builder.build();

    // submit request
    String uriString = uri.toString();
    Log.v(LOG_ID, uriString);
    URL url = new URL(uriString);
    URLConnection connection = url.openConnection();
    if (referer != null) {
      connection.addRequestProperty("Referer", referer);      
    }

    // read response
    BufferedReader reader =
        new BufferedReader(new InputStreamReader(connection.getInputStream()));
    StringBuilder jsonContent = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
      jsonContent.append(line);
    }

    // build JSON result
    JSONObject response = new JSONObject(jsonContent.toString());
    JSONObject responseData = response.getJSONObject("responseData");
    JSONArray arrayResults = responseData.getJSONArray("results");
    Log.v(LOG_ID, "#results = " + arrayResults.length());

    // Convert to standard Java list.
    List<JSONObject> listResults =
        new ArrayList<JSONObject>(arrayResults.length());
    for (int i = 0; i < arrayResults.length(); ++i) {
      JSONObject result = arrayResults.getJSONObject(i);
      listResults.add(result);
    }

    return listResults;
  }
}
